﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Objects;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.Serialization;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Alma.Models;
using Alma.Core;
using AlmaDatabase;

namespace Alma.Controllers
{
    public class SearchController : Controller
    {
        //
        // GET: /Search/

#if false
        private enum ReverseAdvancedState
        {
            Undefined,
            InAnd,
            InOr,
            InNot
        }

        private abstract class RequestExpression
        {
            public abstract IQueryable<Product> Evaluate(IQueryable<Product> query);
            public abstract Expression ToLinqExpression(Expression target);
            public abstract void ReverseAdvanced(AdvancedSearchModel model, ReverseAdvancedState state);
        }

        private class AndExpression : RequestExpression
        {
            public RequestExpression Left { get; set; }
            public RequestExpression Right { get; set; }

            public override IQueryable<Product> Evaluate(IQueryable<Product> query)
            {
                return Right.Evaluate(Left.Evaluate(query));
            }

            public override void ReverseAdvanced(AdvancedSearchModel model, ReverseAdvancedState state)
            {
                Left.ReverseAdvanced(model, ReverseAdvancedState.InAnd);
                Right.ReverseAdvanced(model, ReverseAdvancedState.InAnd);
            }

            public override Expression ToLinqExpression(Expression target)
            {
                return Expression.AndAlso(Left.ToLinqExpression(target), Right.ToLinqExpression(target));
            }
        }

        private class OrExpression : RequestExpression
        {
            public RequestExpression Left { get; set; }
            public RequestExpression Right { get; set; }

            public override IQueryable<Product> Evaluate(IQueryable<Product> query)
            {
                return Left.Evaluate(query).Union(Right.Evaluate(query));
            }

            public override void ReverseAdvanced(AdvancedSearchModel model, ReverseAdvancedState state)
            {
                Left.ReverseAdvanced(model, ReverseAdvancedState.InOr);
                Right.ReverseAdvanced(model, ReverseAdvancedState.InOr);
            }

            public override Expression ToLinqExpression(Expression target)
            {
                return Expression.OrElse(Left.ToLinqExpression(target), Right.ToLinqExpression(target));
            }
        }

        private class SelectorExpression : RequestExpression
        {
            public string Selector { get; set; }
            public string Value { get; set; }
            public string Operator { get; set; }
            public string Operand { get; set; }

            public override IQueryable<Product> Evaluate(IQueryable<Product> query)
            {
                switch (Selector)
                {
                    case "desc":
                        return query.Where(x => x.Description.Contains(Value));
                    case "cat":
                        return query.Where(x => x.Sections.Categories.Name.Contains(Value));
                    case "sec":
                        return query.Where(x => x.Sections.Name.Contains(Value));
                    case "with":
                        //var crit = Value.Split('=');
                        if (Operator == null)
                            throw new ParseException("Opérateur invalide pour le critère '" + Value + "'.");
                        //string critName = crit[0];
                        //string critValue = crit[1];
                        /*
                        return query.ToList().Where(x => x.Product_Criterion.Where(y => y.Criterion.Name == closure).FirstOrDefault() == null
                            || x.Product_Criterion.Where(y => y.Criterion.Name == closure).FirstOrDefault().Value == critValue);
                         */
                        return query.Where(x => x.Product_Criterion.FirstOrDefault(y => y.Criterion.Name == Value) != null
                            && x.Product_Criterion.FirstOrDefault(y => y.Criterion.Name == Value).Value == Operand);
                    default:
                        throw new ParseException("Le sélecteur '" + Selector + "' n'est pas valide.");
                }

            }

            public override void ReverseAdvanced(AdvancedSearchModel model, ReverseAdvancedState state)
            {
                switch (Selector)
                {
                    case "cat":
                        var cat = model.CategoriesItems.SingleOrDefault(x => x.Text == Value);
                        if (cat != null)
                            cat.Selected = true;
                        break;
                    case "sec":
                        var sec = model.SectionItems.SingleOrDefault(x => x.Text == Value);
                        if (sec != null)
                            sec.Selected = true;
                        break;
                }
            }


            public override Expression ToLinqExpression(Expression target)
            {
                //var ex = Expression<
                //return Expression.AndAlso(Left.ToLinqExpression(), Right.ToLinqExpression());
            }
        }

        private class StringExpression : RequestExpression
        {
            public string Value { get; set; }

            public override IQueryable<Product> Evaluate(IQueryable<Product> query)
            {
                return query.Where(x => x.Name.Contains(Value));
            }

            public override void ReverseAdvanced(AdvancedSearchModel model, ReverseAdvancedState state)
            {
                if (state == ReverseAdvancedState.InOr)
                    model.SomeWords += " " + Value;
                else if (state == ReverseAdvancedState.InNot)
                    model.NoneWord += " " + Value;
                else
                {
                    var isOnlyAlpha = true;
                    foreach (var c in Value)
                        if (!char.IsLetterOrDigit(c))
                        {
                            isOnlyAlpha = false;
                            break;
                        }
                    if (isOnlyAlpha)
                        model.AllWords += " " + Value;
                    else
                        model.Expression += Value;
                }
            }

            public override Expression ToLinqExpression(Expression target)
            {
                return Expression.Call(Expression.Property(target, "Name"), typeof(string).GetMethod("Contains"), Expression.Constant(Value));
            }
        }

        private class NotExpression : RequestExpression
        {
            public RequestExpression Inner { get; set; }

            public override IQueryable<Product> Evaluate(IQueryable<Product> query)
            {
                return query.Except(Inner.Evaluate(query));
            }

            public override void ReverseAdvanced(AdvancedSearchModel model, ReverseAdvancedState state)
            {
                Inner.ReverseAdvanced(model, ReverseAdvancedState.InNot);
            }

            public override Expression ToLinqExpression(Expression target)
            {
                return Expression.Not(Inner.ToLinqExpression(target));
            }
        }

        [Serializable]
        public class ParseException : Exception
        {
            public ParseException()
            {
            }

            public ParseException(string message) : base(message)
            {
            }

            public ParseException(string message, Exception inner) : base(message, inner)
            {
            }

            protected ParseException(
                SerializationInfo info,
                StreamingContext context) : base(info, context)
            {
            }
        }

        private StringExpression ParseString(List<string> tokens)
        {
            var r = new StringExpression() { Value = tokens[0]};
            tokens.RemoveAt(0);
            return r;
        }

        private RequestExpression ParseSelector(List<string> tokens)
        {
            var first = ParseString(tokens);
            if (tokens.Count < 2 || tokens[0] != ":")
                return first;
            tokens.RemoveAt(0);
            var second = ParseString(tokens);
            var s = new SelectorExpression() { Selector = first.Value, Value = second.Value };
            if (tokens.Count < 2 || tokens[0] != "=")
                return s;
            s.Operator = tokens[0];
            tokens.RemoveAt(0);
            s.Operand = tokens[0];
            tokens.RemoveAt(0);
            return s;
        }

        private RequestExpression ParseParenthesis(List<string> tokens)
        {
            if (tokens.Count < 3)
                return ParseSelector(tokens);
            if (tokens[0] != "(")
                return ParseSelector(tokens);
            tokens.RemoveAt(0);
            var exp = ParseOr(tokens);
            if (tokens.Count < 1 || tokens[0] != ")")
                throw new ParseException("Il manque une parenthèse droite ')'.");
            tokens.RemoveAt(0);
            return exp;
        }

        private RequestExpression ParseNot(List<string> tokens)
        {
            if (tokens.Count < 2)
                return ParseParenthesis(tokens);
            if (tokens[0] != "-")
                return ParseParenthesis(tokens);
            tokens.RemoveAt(0);
            return new NotExpression() { Inner = ParseNot(tokens) };
        }

        private RequestExpression ParseAnd(List<string> tokens)
        {
            var first = ParseNot(tokens);
            if (tokens.Count < 1)
                return first;
            if (tokens[0] != "+" && (!char.IsLetterOrDigit(tokens[0][0]) &&
                tokens[0] != "-" && tokens[0] != "("))
                return first;
            if (tokens[0] == "+")
                tokens.RemoveAt(0);
            return new AndExpression { Left = first, Right = ParseAnd(tokens) };
        }

        private RequestExpression ParseOr(List<string> tokens)
        {
            var first = ParseAnd(tokens);
            if (tokens.Count < 2)
                return first;
            if (tokens[0] != "|")
                return first;
            tokens.RemoveAt(0);
            return new OrExpression { Left = first, Right = ParseOr(tokens) };
        }

        private RequestExpression ParseRequest(List<string> tokens)
        {
            var exp = ParseOr(tokens);
            if (tokens.Count == 0)
                return exp;
            if (tokens[0] == ")")
                throw new ParseException("Il manque une parenthèse gauche '('.");
            throw new ParseException("Requête erronée.");
        }
#endif
        [HttpGet]
        public ActionResult Index(string s)
        {
            var search = new SearchModel() {Request = s};

            if (s == null)
                return View(search);

            //var tokens = search.Request.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries);
            var tokens = SearchTokenizer.Tokenise(search.Request);

            if (tokens.Count > 0)
            {
                using (var context = new AlmaDBEntities())
                {
                    IQueryable<Product> request = context.Products;

                    try
                    {
                        var exp = SearchParser.ParseRequest(tokens);
                        var builder = new SearchLinqBuilder(context);
                        //request = exp.Evaluate(request);
                        exp.Accept(builder);
                        Debug.WriteLine(((ObjectQuery)request.Where(builder.Result)).ToTraceString());
                        search.Results = request.Where(builder.Result).ToList();
                    }
                    catch (SearchException ex)
                    {
                        search.ErrorMessage = ex.Message;
                    }
                    
                }
            }
            return View(search);
        }

        public ActionResult Advanced(string s)
        {
            var model = new AdvancedSearchModel();

            model.CategoriesItems = new List<SelectListItem>();
            model.SectionItems = new List<SelectListItem>();
            model.Criterions = new List<CriterionComparer>();

            using (var context = new AlmaDBEntities())
            {
                foreach (var cat in context.Categories)
                {
                    model.CategoriesItems.Add(new SelectListItem()
                                                  {
                                                      Text = cat.Name,
                                                      //Value = cat.ID.ToString()
                                                  });
                }
                foreach (var sec in context.Sections)
                {
                    model.SectionItems.Add(new SelectListItem()
                    {
                        Text = sec.Name,
                        //Value = cat.ID.ToString()
                    });
                }
            }

            if (s != null)
            {
                var tokens = SearchTokenizer.Tokenise(s);

                if (tokens.Count > 0)
                {
                    var exp = SearchParser.ParseRequest(tokens);
                    var analyzer = new SearchAnalyzer(model);
                    exp.Accept(analyzer);
                    model.TrimAll();

                    //exp.ReverseAdvanced(model, ReverseAdvancedState.Undefined);
                }

            }

            model.Criterions.Add(new CriterionComparer());

            return View(model);
        }

        [HttpPost]
        public ActionResult Advanced(AdvancedSearchModel model)
        {

            var request = new StringBuilder();

            if (model.AllWords != null)
                request.Append(model.AllWords);

            if (model.Expression != null)
            {
                request.Append(" \"");
                request.Append(model.Expression);
                request.Append('"');
            }

            if (model.SomeWords != null)
            {
                request.Append(" (");
                request.Append(model.SomeWords.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).
                    Aggregate((x, acc) => acc + " | " + x));
                request.Append(')');
            }

            if (model.NoneWord != null)
            {
                request.Append(' ');
                request.Append(model.NoneWord.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries).
                    Select((x) => "-" + x).Aggregate((x, acc) => acc + " " + x));
            }

            if (model.Category != null)
            {
                request.Append(" cat:\"");
                request.Append(model.Category);
                request.Append('"');
            }

            if (model.Section != null)
            {
                request.Append(" sec:\"");
                request.Append(model.Section);
                request.Append('"');
            }

            for (int i = 1, max = int.Parse(Request["CriterionCount"]); i <= max; i++)
            {
                //var toto = Request["CriterionTarget2"];

                var tmp = Request["CriterionTarget" + i];

                // WTF, i'm obligated to remove the ',' that appears from nothing in the string
                // may be a MVC3 problem...
                //tmp = tmp.Substring(0, tmp.Length - 1);

                if (tmp != "")
                {
                    request.Append(" @");
                    request.Append(tmp);
                    tmp = Request["CriterionOperator" + i];
                    //tmp = tmp.Substring(0, tmp.Length - 1);
                    request.Append(tmp);

                    tmp = Request["CriterionValue" + i];
                    //tmp = tmp.Substring(0, tmp.Length - 1);
                    request.Append(tmp);
                }
            }

            var finalRequest = request.ToString().Trim();

            if (Request["Action"] == "AddCriterion")
                return RedirectToAction("Advanced", new RouteValueDictionary() { { "s", finalRequest } });
            else
                return RedirectToAction("Index", new RouteValueDictionary() { { "s", finalRequest } });
        }

    }
}
